package com.google.code.lf.commons.xml.bind.helper;

import java.io.File;
import java.io.OutputStream;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.google.code.lf.commons.exception.NullArgumentException;
import com.google.code.lf.commons.util.FileHelper;
import com.sun.xml.bind.marshaller.CharacterEscapeHandler;

/**
 * Classe regroupant des méthodes utilitaires pour la manipulation de fichiers avec l'api <a href="https://jaxb.dev.java.net/">JAXB</a>.
 * 
 * @author gael.lorent-fonfrede
 */
public class XmlFileHelper {
	/** Logger. */
	private static final Logger logger = Logger.getLogger(XmlFileHelper.class);

	/**
	 * Constructeur. Ne doit pas être appelé.
	 * @throws UnsupportedOperationException Toujours.
	 */
	private XmlFileHelper() {
		throw new UnsupportedOperationException("Classe utilitaire, ne doit pas être instanciée.");
	}

	/**
	 * Valide le paramètre 'encoding' fourni.
	 * <br/>Vérifie : non-<code>null</code>, valeur fournie = valeur attendue.
	 * <br/>Un warning est tracé en cas de différence.
	 * 
	 * @param encoding La valeur d'encodage fournie.
	 * @param expectedEncoding La valeur d'encodage attendue.
	 */
	public static void checkParamEncoding(final String encoding, final String expectedEncoding) {
		if ((encoding != null) && !expectedEncoding.equals(encoding)) logger.warn("Provided parameter 'encoding' value has unexpected value. Expected values are : null (defaults to utf-8), " + expectedEncoding);
	}

	/**
	 * Initialise une instance de {@link JAXBContext} en y associant un certain nombre de classes model.
	 * 
	 * @param classesToBeBound La liste de {@link Class} à associer au {@link JAXBContext}.
	 * @return Une instance de {@link JAXBContext} initialisée.
	 */
	public static JAXBContext initializeJaxbContext(final Class<?>...classesToBeBound) {
		try {
			// Initialisation du contexte JAXB
			return JAXBContext.newInstance(classesToBeBound);
		} catch (final JAXBException e) {
			logger.fatal(XmlFileHelper.getJaxbExceptionErrMsg(e).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	/**
	 * Effectue la lecture et le parsing (unmarshalling) du fichier xml fourni pour en retourner le bean model correspondant.
	 * 
	 * @param jaxbContext Le {@link JAXBContext}, initialisé au préalable.
	 * @param xmlFile Le {@link File} à lire (attendu au format xml).
	 * @param expectedType La {@link Class} du type d'objet attendu, nécessairement associé à l'instance 'jaxbContext'.
	 * @return Un bean model de type spécifié 'expectedType', représentant le contenu du fichier lu.
	 * 
	 * @see #initializeJaxbContext(Class...)
	 */
	@SuppressWarnings("unchecked")
	public static <T> T readFile(final JAXBContext jaxbContext, final File xmlFile, final Class<T> expectedType) {
		// Validation des paramètres
		if (jaxbContext == null) throw new NullArgumentException("jaxbContext", "Provided parameter is null. You could use 'XmlFileHelper.initializeJaxbContext(Class...)' before.");
		FileHelper.checkFileExistsAndIsReadable(xmlFile);

		// Lecture et parsing (unmarshalling) du fichier xml fourni
		Object o = null;
		try {
			final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
			o = unmarshaller.unmarshal(xmlFile);
		} catch (final JAXBException e) {
			logger.fatal(XmlFileHelper.getJaxbExceptionErrMsg(e).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}

		// Validation du type attendu
		T model = null;
		if (o.getClass() == expectedType) model = (T) o;
		else throw new IllegalStateException(new StringBuilder("Unmarshalled object is not of expected type ; expected: ").append(expectedType).append(", found: ").append(o.getClass()).append(".").toString());

		// Retourne l'objet de type spécifié
		return model;
	}

	/**
	 * Initialisation de l'instance de {@link Marshaller}, et notamment de ses propriétés.
	 * <br/>Gestion des propriétés : <ul>
	 * <li>encodage,
	 * <li>formatage du flux xml,
	 * <li>valeur de la chaîne d'indentation,
	 * <li>gestionnaire d'échappement de caractères</ul>.
	 * 
	 * @param jaxbContext Le {@link JAXBContext}, initialisé au préalable.
	 * @param formatOutput Flag indiquant si le flux xml doit être formaté (sauts de ligne, indentation). Valeur par défaut dans l'api <a href="https://jaxb.dev.java.net/">JAXB</a> : <code>false</code>.
	 * @param encoding L'encodage à utiliser (facultatif, peut être <code>null</code>). Valeur par défaut dans l'api <a href="https://jaxb.dev.java.net/">JAXB</a> : 'UTF-8'.
	 * @param indentString Chaîne d'indentation (facultatif, peut être <code>null</code>). Valeur par défaut dans l'api <a href="https://jaxb.dev.java.net/">JAXB</a> : "    ".
	 * @param characterEscapeHandler Instance de {@link CharacterEscapeHandler} à associer à l'instance de marshaller (facultatif, peut être <code>null</code>). Valeur par défaut dans l'api <a href="https://jaxb.dev.java.net/">JAXB</a> : celle de la JRE.
	 * 
	 * @see #initializeJaxbContext(Class...)
	 */
	public static Marshaller initializeMarshaller(final JAXBContext jaxbContext, final boolean formatOutput, final String encoding, final String indentString, final Object characterEscapeHandler) {
		// Validation des paramètres
		if (jaxbContext == null) throw new NullArgumentException("jaxbContext", "Provided parameter is null. You could use 'XmlFileHelper.initializeJaxbContext(Class...)' before.");

		try {
			// Initialisation du marshaller
			final Marshaller marshaller = jaxbContext.createMarshaller();

			// Encodage utilisé
			if (StringUtils.isNotBlank(encoding)) marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
			// Formatage du flux xml (sauts de ligne, identation)
			if (formatOutput) marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			// Valeur de l'indentation : Spécificité Grisbi (la valeur par défaut est : "    "), la changer permet d'éclaircir les résultats de tests unitaires
			if (StringUtils.isNotBlank(indentString)) marshaller.setProperty("com.sun.xml.bind.indentString", "  ");
			// Gestion des caractères échappés (notamment pour les diacritiques accentués : échappement en valeur hexadécimale plutôt qu'en numérique unicode)
			if ((characterEscapeHandler != null) && (characterEscapeHandler instanceof CharacterEscapeHandler)) marshaller.setProperty("com.sun.xml.bind.characterEscapeHandler", characterEscapeHandler);

			return marshaller;
		} catch (final JAXBException e) {
			logger.fatal(XmlFileHelper.getJaxbExceptionErrMsg(e).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	/**
	 * Ecriture d'un bean model sous forme de fichier au format xml.
	 * 
	 * @param marshaller L'instance de {@link Marshaller} à utiliser pour écrire l'objet au format xml, initialisée au préalable.
	 * @param model L'objet model à écrire (marshaller).
	 * @param xmlFile L'instance de {@link File} à utiliser pour écrire l'objet.
	 * 
	 * @see #initializeMarshaller(JAXBContext, boolean, String, String, Object)
	 */
	public static void writeFile(final Marshaller marshaller, final Object model, final File xmlFile) {
		// Validation des paramètres
		if (marshaller == null) throw new NullArgumentException("marshaller", "Provided parameter is null. You could use 'XmlFileHelper.initializeMarshaller(JAXBContext, boolean, String, String, Object)' before.");
		if (model == null) throw new NullArgumentException("object");
		FileHelper.checkFileExistsAndIsReadable(xmlFile);

		// Ecriture de l'objet fourni
		try {
			marshaller.marshal(model, xmlFile);
		} catch (final JAXBException e) {
			logger.fatal(XmlFileHelper.getJaxbExceptionErrMsg(e).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	/**
	 * Ecriture d'un bean model sous forme d'{@link OutputStream} (par exemple {@link System#out}, pour les tests).
	 * 
	 * @param marshaller L'instance de {@link Marshaller} à utiliser pour écrire l'objet au format xml, initialisée au préalable.
	 * @param model L'objet model à écrire (marshaller).
	 * @param outputStream L'instance de {@link OutputStream} à utiliser pour écrire l'objet. A des fins de tests, {@link System#out} est utilisable pour une sortie en console.
	 * 
	 * @see #initializeMarshaller(JAXBContext, boolean, String, String, Object)
	 */
	public static void writeFile(final Marshaller marshaller, final Object model, final OutputStream outputStream) {
		// Validation des paramètres
		if (marshaller == null) throw new NullArgumentException("marshaller", "Provided parameter is null. You could use 'XmlFileHelper.initializeMarshaller(JAXBContext, boolean, String, String, Object)' before.");
		if (model == null) throw new NullArgumentException("object");
		if (outputStream == null) throw new NullArgumentException("outputStream", "Provided parameter is null. You could use 'System.out'.");

		// Ecriture de l'objet fourni
		try {
			marshaller.marshal(model, outputStream);
		} catch (final JAXBException e) {
			logger.fatal(XmlFileHelper.getJaxbExceptionErrMsg(e).toString());
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	/**
	 * Génère le message d'erreur à partir d'une {@link JAXBException}.
	 * 
	 * @param e La {@link JAXBException}.
	 * @return Le message d'erreur.
	 */
	public static StringBuilder getJaxbExceptionErrMsg(final JAXBException e) {
		final StringBuilder errMsg = new StringBuilder("JAXBException raised:");
		if (e.getErrorCode() != null) errMsg.append(" errCode: ").append(e.getErrorCode()).append(",");
		errMsg.append(" errMsg: ").append(e.getMessage());
		if (e.getCause() != null) errMsg.append(", errCause: ").append(e.getCause().getMessage());
		return errMsg;
	}
}
